# python 3.10.5
import concurrent
import re
import logging
from collections import defaultdict
from datetime import datetime, timedelta, date
import json
from netmiko import ConnectHandler
import time
import os
from concurrent.futures import ThreadPoolExecutor
import openpyxl
import csv
from peewee import *

DELIMITER = '\n'
CHECK_PASS = 'OK'


def int_log():
    # 创建一个日志记录器
    logger_ = logging.getLogger('network')
    logger_.setLevel(logging.INFO)
    if not os.path.exists('log'):
        os.makedirs('log')
    # 创建文件处理器，指定以追加模式写入
    file_handler = logging.FileHandler('log/app.log', mode='w')
    file_handler.setLevel(logging.DEBUG)

    # 创建控制台处理器
    console_handler = logging.StreamHandler()
    console_handler.setLevel(logging.DEBUG)

    # 定义日志格式
    formatter = logging.Formatter('%(asctime)s - %(levelname)s - %(message)s')
    file_handler.setFormatter(formatter)
    console_handler.setFormatter(formatter)

    # 将处理器添加到日志记录器
    logger_.addHandler(file_handler)
    logger_.addHandler(console_handler)
    return logger_


if not os.path.exists('data'):
    os.makedirs('data')

logger = int_log()

# 连接到 SQLite 数据库，这里指定数据库文件名为 example.db
db = SqliteDatabase(os.path.join('data', 'network.db'))


# 定义一个基类，用于继承，方便后续操作
class BaseModel(Model):
    class Meta:
        database = db


# 定义一个具体的模型类，对应数据库中的表
class Network(BaseModel):
    id = AutoField()  # 定义自动增长的整数主键
    ip = CharField(column_name='ip', null=False)
    deviceName = CharField(column_name='device_name', null=False)
    manufacturer = CharField(column_name='manufacturer', null=False)
    metric = CharField(column_name='metric', null=False)
    cmd = CharField(column_name='cmd', null=False)
    result = TextField(column_name='result', default=None, null=True)
    interval = IntegerField(default=1)


def create_table_if_not_exists(model_class):
    """
    检查模型对应的表是否存在，如果不存在则创建表
    :param model_class: 模型类
    """
    global db
    if not model_class.table_exists():
        db.create_tables([model_class])


def load_json_file(file_path) -> dict:
    try:
        # 以只读模式打开指定路径的 JSON 文件
        with open(file_path, 'r', encoding='utf-8') as file:
            # 使用 json.load 函数将文件内容解析为 Python 对象
            data = json.load(file)
            return data
    except FileNotFoundError:
        logger.error(f"错误：未找到文件 {file_path}。")
    except json.JSONDecodeError:
        logger.error(f"错误：无法解析 {file_path} 中的 JSON 数据。")
    except Exception as e:
        logger.error(f"发生未知错误：{e}")
    return {}


create_table_if_not_exists(Network)


config: dict = load_json_file('metric.json')
if not config:
    exit(0)

H3C_ERROR_MSG = ("doesn't exist", 'not configured', 'Unrecognized command')
HUAWEI_ERROR_MSG = ("Unrecognized command", 'not enabled', 'Error:')


def is_ip_address_mask(s):
    pattern = r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2}$'
    if re.match(pattern, s):
        parts = s.split('/')
        ip_parts = parts[0].split('.')
        for part in ip_parts:
            if not (0 <= int(part) <= 255):
                return False
        mask = int(parts[1])
        return 1 <= mask <= 32
    return False


def starts_with_ip(s):
    pattern = r'^\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}'
    if re.match(pattern, s):
        parts = s.split('.', 3)
        for part in parts[:3]:
            try:
                num = int(part)
                if num < 0 or num > 255:
                    return False
            except ValueError:
                return False
        return True
    return False


class Device(object):

    def __init__(self) -> None:
        self.name = None  # 设备名称
        self.type = None  # 设备类型
        self.manufacturer = None  # 厂商
        self.ip = None  # ip地址
        self.username = None  # 登陆用户
        self.password = None  # 登陆密码
        self.port = 55522

    def __str__(self) -> str:
        return f'设备命令: {self.name} 厂商: {self.manufacturer} 地址: {self.ip} 用户名: {self.password}: 密码: {self.password}'


class Cmd(object):
    def __init__(self):
        self.deviceName = None
        self.manufacturer = ''  # 厂商
        self.ip = None
        self.metric = None
        self.cmd = None  # 命令
        self.rawData = None  # 采集结果
        self.prevResult = None  # 上一次采集结果
        self.result = None  # 本次结果
        self.analyze = '不涉及'  # 分析结论
        self.log = None
        self.extra_specs = {}

    def __str__(self) -> str:
        return f'设备: {self.deviceName} 检查项: {self.metric} 厂商: {self.manufacturer} {self.ip} cmd: {self.cmd}'

    def to_db(self, interval: int):
        return {'ip': self.ip, 'deviceName': self.deviceName, 'manufacturer': self.manufacturer, 'metric': self.metric, 'cmd': self.cmd,
                'result': self.result, 'interval': interval}


class HuaweiSwitchAnalyzer(object):
    @staticmethod
    def calc_minutes(uptime_str):
        # 正则表达式匹配天数、小时数和分钟数
        if 'week' in uptime_str:
            pattern1 = r'(\d+) weeks?, (\d+) days?, (\d+) hours?, (\d+) minutes?'
        else:
            pattern1 = r'(\d+) days?, (\d+) hours?, (\d+) minutes?'
        match = re.search(pattern1, uptime_str)
        if match:
            weeks = 0
            if len(match.groups()) == 4:
                weeks, days, hours, minutes = map(int, match.groups())
            else:
                days, hours, minutes = map(int, match.groups())
            return weeks * 7 * 24 * 60 + days * 24 * 60 + hours * 60 + minutes
        return 0

    @staticmethod
    def split_line(raw_data: str) -> list:
        if not raw_data:
            return []
        lines = raw_data.split('\n')
        if len(lines) < 3:
            return []
        return lines

    @staticmethod
    def fetch_version(cmd: Cmd):
        """
        获取交换机/路由器/防火墙版本
        :param cmd: display version
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        for line in lines:
            line = line.strip()
            if 'Version' in line:
                if line.startswith('Version'):
                    cmd.analyze = line
                else:
                    cmd.analyze = line.split(',')[-1]
                return

    @staticmethod
    def fetch_m_lag_status(cmd: Cmd):
        """
        设备状态检查1
        :param cmd: display dfs-group 1 m-lag
        :return:
        """
        master = False
        slave = False
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        if len(lines) < 5:
            return
        for line in lines:
            line = line.strip()
            if 'Backup' in line:
                slave = True
            elif 'Master' in line:
                master = True
        if master and slave:
            cmd.analyze = CHECK_PASS
        elif not master:
            cmd.analyze = 'State: Master找到(display dfs-group 1 m-lag)'
        elif not slave:
            cmd.analyze = 'State: Slave找到(display dfs-group 1 m-lag)'
        else:
            cmd.analyze = 'State: Master/State: Slave均未找到(display dfs-group 1 m-lag)'

    @staticmethod
    def fetch_m_lag_port(cmd: Cmd):
        """
        设备状态检查2
        :param cmd: display dfs-group 1 m-lag brief
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        error_line = ''
        for line in lines:
            line = line.strip()
            if 'Eth-' in line and 'Up' not in line:
                error_line += f'{line}{DELIMITER}'
            elif 'Connection state:' in line and 'succeeded' not in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_firewall_status(cmd: Cmd):
        """
        防火墙双组状态检查
        :param cmd: display hrp state
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        error_line = ''
        for line in lines:
            line = line.strip()
            if 'Connection state:' in line and 'succeeded' not in line:
                error_line = f'{line}{DELIMITER}'
                break
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_stack_status(cmd: Cmd):
        """
        设备状态检查3
        :param cmd: display stack topology
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        pattern = r'^Stack-Port\d'
        error_line = ''
        for line in lines:
            line = line.strip()
            if bool(re.match(pattern, line)):
                parts = line.split()
                if parts.count('up') != 2:
                    error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_runtime(cmd: Cmd):
        """
        运行时间
        :param cmd: display version
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        run_time = 0
        for line in lines:
            line = line.strip()
            if 'uptime' in line:
                chips = line.split('uptime is')
                run_time = HuaweiSwitchAnalyzer.calc_minutes(chips[1])
                break
        prev_result = int(cmd.prevResult) if cmd.prevResult else 0
        cmd.result = str(run_time)
        cmd.analyze = '设备重启' if run_time < prev_result else CHECK_PASS

    @staticmethod
    def fetch_fan(cmd: Cmd):
        """
        风扇检查
        :param cmd: display device
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        for line in lines:
            line = line.strip()
            if 'FAN' in line and 'Normal' not in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_power(cmd: Cmd):
        """
        电源检查
        :param cmd: display device
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        for line in lines:
            line = line.strip()
            if 'PWR' in line and 'Normal' not in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_three_lay_check(cmd: Cmd):
        """
        三层接口状态
        :param cmd: display ip interface brief
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        start_index = next((i for i, line in enumerate(lines) if line.startswith('Interface')), None)
        if start_index is not None:
            port_lines = lines[start_index + 1:]
            for line in port_lines:  # str
                line = line.strip()
                if not line or line.startswith('MEth'):
                    continue
                parts = line.split()
                if len(parts) >= 4 and is_ip_address_mask(parts[1]):
                    physical_status = parts[2].strip()
                    protocol_status = parts[3].strip()
                    if not ('up' in physical_status and 'up' in protocol_status):
                        error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or 'OK'

    @staticmethod
    def fetch_port_check(cmd: Cmd):
        """
        端口检查
        :param cmd: display interface brief
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        start_index = next((i for i, line in enumerate(lines) if line.startswith('Interface')), None)
        if start_index is not None:
            port_lines = lines[start_index + 1:]
            enable_check = False
            for line in port_lines:
                if line.startswith('Eth-Trunk'):
                    enable_check = True
                    continue
                if enable_check and not line.startswith(' '):
                    enable_check = False

                chips = line.split()
                port: str = chips[0].strip()
                if len(chips) > 2 and (port.endswith('47') or port.endswith('48') or enable_check):
                    status1 = chips[1].lower()
                    status2 = chips[2].lower()
                    if not ('up' in status1 and 'up' in status2):
                        error_line += f'{line}{DELIMITER}'

        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def extract_port(cmd: Cmd) -> set:
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return {[]}
        ports = {[]}
        start_index = next((i for i, line in enumerate(lines) if line.startswith('Interface')), None)
        if start_index is not None:
            port_lines = lines[start_index + 1:]
            enable_check = False
            for line in port_lines:
                if line.startswith('Eth-Trunk'):
                    enable_check = True
                    continue
                if enable_check and not line.startswith(' '):
                    enable_check = False

                chips = line.split()
                port: str = chips[0].strip()
                if len(chips) > 2 and (port.endswith('47') or port.endswith('48') or enable_check):
                    ports.add(port)
        cmd.result = ports

    @staticmethod
    def fetch_port_utility(cmd: Cmd):
        """
        端口使用率
        :param cmd: display interface brief
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_port = ''
        header_index = next((i for i, line in enumerate(lines) if 'Interface' in line), None)
        if header_index is not None:
            for line in lines[header_index + 1:]:
                line = line.strip()
                if not line:
                    continue
                parts = line.split()
                interface = parts[0]
                if interface.startswith('Eth-Trunk') or interface.startswith('LoopBack') or \
                        interface.startswith('MEth') or interface.startswith('NULL') or interface.startswith('Sip') or \
                        interface.startswith('Vlan'):
                    continue
                if 'up' in line or 'UP' in line:
                    in_uti = float(parts[3].strip().strip('%'))
                    out_uti = float(parts[4].strip().strip('%'))
                    if in_uti > 50 or out_uti > 50:
                        error_port += f'{interface}使用率高于50{DELIMITER}'
        cmd.analyze = error_port or CHECK_PASS

    @staticmethod
    def fetch_configure(cmd: Cmd):
        """
        配置文件检查
        :param cmd: dir
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        prev_size = cmd.prevResult
        size = ''
        for line in lines:
            line = line.strip()
            if 'vrpcfg.zip' in line:
                try:
                    size = line.split()[2]
                except ValueError:
                    logger.error("read config size failed, %s", line)
                break
        cmd.result = size
        cmd.analyze = '文件大小发生变化, 请手动检查' if prev_size and prev_size != size else CHECK_PASS

    @staticmethod
    def fetch_cpu_utility(cmd: Cmd):
        """
        CPU利用率
        :param cmd: display cpu
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        system_cpu_usage = -1
        max_cpu_usage = -1
        for line in lines:
            if system_cpu_usage != -1 and max_cpu_usage != -1:
                break
            line = line.strip()
            if not line:
                continue
            chips = line.split(':', 1)
            try:
                if "System CPU Using Percentage" in line:
                    system_cpu_usage = int(line.split(':')[-1].strip().strip('%'))
                elif "Max CPU Usage" in line and "Stat. Time" not in line:
                    max_cpu_usage = int(line.split(':')[-1].strip().strip('%'))
                elif chips[0].strip() == 'CPU Usage':
                    chips1 = chips[1].strip().split('Max:')
                    system_cpu_usage = int(chips1[0].strip().strip('%'))
                    max_cpu_usage = int(chips1[1].strip().strip('%'))
            except Exception as e:
                logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                raise e
        cmd.analyze = f"CPU使用率过高, 平均使用率{system_cpu_usage}%, 最大使用率{max_cpu_usage}%" if system_cpu_usage > 70 or max_cpu_usage > 90 else CHECK_PASS

    @staticmethod
    def fetch_memory_utility(cmd: Cmd):
        """
        内存利用率
        :param cmd: display memory
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        memory_percentage = -1
        for line in lines:
            line = line.strip()
            if not line:
                continue
            try:
                if "Memory Using Percentage" in line:
                    memory_percentage_str = line.split(':')[-1].strip().strip('%')
                    memory_percentage = int(memory_percentage_str)
                    break
            except Exception as e:
                logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                raise e
        cmd.analyze = f"内存使用率过高, 平均使用率{memory_percentage}%" if memory_percentage > 70 else CHECK_PASS

    @staticmethod
    def fetch_device_alarm(cmd: Cmd):
        """
        设备当前告警记录
        :param cmd: display alarm active
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        # 获取当前时间
        now = datetime.now()

        # 解析告警内容
        alarms = []
        pattern = re.compile(r'\d{2}:\d{2}:\d{2}')
        start_index = next((i for i, line in enumerate(lines) if 'Sequence' in line), None)
        alarm_content = -1
        filtered_alarms = []
        if start_index is not None:
            i = start_index + 2
            while i < len(lines):
                line = lines[i]
                if line.startswith('-----'):
                    break
                if line and line[0].isdigit():
                    line = line.strip()
                    parts = line.split()
                    alarm_content += 1
                    alarms.append({
                        'sequence': parts[0],
                        'alarm_id': parts[1],
                        'severity': parts[2],
                        'time': parts[3],
                        'description': ' '.join(parts[4:])
                    })
                else:
                    matched = re.search(pattern, line)
                    line = line.strip()
                    if matched:
                        alarms[alarm_content]['time'] += f' {matched.group(0)}'
                        alarms[alarm_content]['description'] += line.split(maxsplit=1)[1]
                    else:
                        alarms[alarm_content]['description'] += line
                i += 1

            # 过滤出 Critical 和 Major 级别且时间在 24 小时以内的告警
            for alarm in alarms:
                try:
                    if alarm['severity'] in ['Critical', 'Major'] and (
                            now - datetime.strptime(alarm['time'], "%Y-%m-%d %H:%M:%S")) <= timedelta(hours=24):
                        filtered_alarms.append(
                            "{0} {1} {2} {3} {4}".format(alarm['sequence'], alarm['alarm_id'], alarm['severity'],
                                                         alarm['time'], alarm['description']))
                except Exception as e:
                    logger.error("Date compare failed, ip: %s, cmd: %s, %s, %s", cmd.ip, cmd.cmd, alarm['time'], e)
        cmd.analyze = f'{DELIMITER}'.join(filtered_alarms) if filtered_alarms else CHECK_PASS

    @staticmethod
    def fetch_single_board_status(cmd: Cmd):
        """
        告警记录条目
        :param cmd: display device
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_slot = ''
        header_index = next((i for i, line in enumerate(lines) if 'Slot' in line), None)
        if header_index is not None:
            for line in lines[header_index + 2:]:
                line = line.strip()
                if '---' in line:
                    break
                try:
                    if line:
                        if 'Normal' not in line:
                            error_slot += f'{line}{DELIMITER}'
                except Exception as e:
                    logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                    raise e
        cmd.analyze = error_slot or CHECK_PASS

    @staticmethod
    def fetch_rx_power(cmd: Cmd):
        """
        收发光功率
        :param cmd: display interface transceiver brief
        :return:
        """
        if not cmd.rawData:
            return
        error_port = {[]}
        if isinstance(cmd.rawData, str):
            lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
            if not lines:
                return
            current_port = None
            need_check_port = cmd.extra_specs.get('ports')
            i = 0
            while i < len(lines):
                line = lines[i].strip()
                if line.strip().endswith("transceiver diagnostic information:"):
                    current_port = line.split()[0]
                elif "Current RX Power (dBm)" in line:
                    if 'Lane' in line:
                        has_error_port = any(float(e) < -8 for e in line.split(':')[1].split()[0].split('|'))
                        i += 1
                        if has_error_port or any(float(e) < -8 for e in lines[i].strip().split()[0].split('|')):
                            error_port.add(current_port)
                    else:
                        power_str = line.split(':')[-1].strip()
                        try:
                            power = float(power_str)
                            if power < -8:
                                error_port.add(current_port)
                        except ValueError:
                            pass
                i += 1
            if need_check_port:
                error_port = error_port - need_check_port
        else:
            pattern = r'^Rx\d* Power'
            float_pattern = r'-?\d+\.\d'
            for port, content in cmd.rawData.items():
                for line in content.split('\n'):
                    if not bool(re.match(pattern, line)):
                        continue
                    match = re.search(float_pattern, line)
                    try:
                        if float(match.group(0)) < -8:
                            error_port.add(port)
                            break
                    except ValueError:
                        break

        cmd.analyze = f'{DELIMITER}'.join(error_port) if error_port else CHECK_PASS

    @staticmethod
    def fetch_ospf_status(cmd: Cmd):
        """
        路由OSPF协议检查
        :param cmd: display ospf peer brief
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_interface = ''
        header_index = next((i for i, line in enumerate(lines) if 'Area Id' in line), None)
        if header_index is not None:
            for line in lines[header_index + 1:]:
                if '---' in line:
                    break
                try:
                    line = line.strip()
                    if line.strip():
                        parts = line.split()
                        state = parts[3]
                        if state != 'Full':
                            error_interface += f'{parts[1]}{DELIMITER}'
                except Exception as e:
                    logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                    raise e
        else:
            error_interface = '/'
        cmd.analyze = error_interface or CHECK_PASS

    @staticmethod
    def fetch_bgp_status(cmd: Cmd):
        """
        路由BGP协议检查
        :param cmd: display bgp peer
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_peer = ''
        header_index = next((i for i, line in enumerate(lines) if 'Peer ' in line), None)
        if header_index is not None:
            for line in lines[header_index + 1:]:
                line = line.strip()
                if line:
                    parts = line.split()
                    if len(parts) >= 8:
                        state = parts[7]
                        if state != 'Established':
                            error_peer += f'{parts[0]}{DELIMITER}'
        else:
            error_peer = '/'
        cmd.analyze = error_peer or CHECK_PASS

    @staticmethod
    def fetch_ospf_item_status(cmd: Cmd):
        """
        路由OSPF条目检查
        :param cmd: display ospf routing
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        if cmd.prevResult:
            prev_result = set(json.loads(cmd.prevResult))
        else:
            prev_result = set()
        result = set()
        for line in lines:
            line = line.strip()
            if starts_with_ip(line):
                result.add(line.split(' ', 1)[0].strip())
        router = []
        router.extend(prev_result - result)
        router.extend(result - prev_result)
        cmd.result = json.dumps(list(result))
        cmd.analyze = f'{DELIMITER}'.join(router) if router else CHECK_PASS

    @staticmethod
    def fetch_bgp_item_status(cmd: Cmd):
        """
        路由GBP条目检查
        :param cmd: display bgp routing-table
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        header_index = next((i for i, line in enumerate(lines) if 'Network' in line), None)
        if cmd.prevResult:
            prev_result = set(json.loads(cmd.prevResult))
        else:
            prev_result = set()
        result = set()
        if header_index is not None:
            for line in lines[header_index + 1:]:
                if '>' in line:
                    parts = line.split()
                    # 提取 Network 列的值
                    network = parts[1] if len(parts) > 1 else None
                    if network:
                        result.add(network)
        router = []
        router.extend(prev_result - result)
        router.extend(result - prev_result)
        cmd.result = json.dumps(list(result))
        cmd.analyze = f'{DELIMITER}'.join(router) if router else CHECK_PASS

    @staticmethod
    def fetch_ip_table_status(cmd: Cmd):
        """
        路由表条目检查
        :param cmd: display ip routing-table
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        if cmd.prevResult:
            prev_result = set(json.loads(cmd.prevResult))
        else:
            prev_result = set()
        result = set()
        for line in lines:
            line = line.strip()
            if starts_with_ip(line):
                result.add(line.split(maxsplit=1)[0].strip())
        router = []
        router.extend(prev_result - result)
        router.extend(result - prev_result)
        cmd.result = json.dumps(list(result))
        cmd.analyze = f'{DELIMITER}'.join(router) if router else CHECK_PASS

    @staticmethod
    def fetch_m_lag_alarm(cmd: Cmd):
        """
        M_LAG 24小时告警记录
        :param cmd: display m-lag troubleshooting history
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        header_index = next((i for i, line in enumerate(lines) if 'Event Description' in line), None)
        now = datetime.now()
        # 计算 24 小时前的时间
        events = list()
        start_time = now - timedelta(hours=24)
        if header_index is not None:
            lines = lines[header_index + 2:]
            i = 0
            while i < len(lines):
                line = lines[i].strip()
                if line and line[0].isdigit():
                    parts = line.split(maxsplit=3)
                    seq = parts[0]
                    date_str = parts[1]
                    time_str = parts[2]
                    desc = parts[3]
                    i += 1
                    # 处理描述换行
                    while i < len(lines) and not lines[i].startswith('---') and not lines[i][0].isdigit():
                        desc += ' ' + lines[i].strip()
                        i += 1
                    try:
                        event_time = datetime.strptime(f'{date_str} {time_str}', '%Y-%m-%d %H:%M:%S')
                        if event_time >= start_time:
                            events.append((seq, event_time.strftime('%Y-%m-%d %H:%M:%S'), desc))
                    except Exception as e:
                        logger.error("parse time failed, ip:%s cmd: line: %s, %s", cmd.ip, cmd.cmd, line, e)
                else:
                    i += 1
        cmd.analyze = f'{DELIMITER}'.join(' '.join(e) for e in events) if events else CHECK_PASS

    @staticmethod
    def fetch_stack_alarm(cmd: Cmd):
        """
        堆叠 24小时告警记录
        :param cmd: display stack troubleshooting history
        :return:
        """
        lines = HuaweiSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        header_index = next((i for i, line in enumerate(lines) if 'Event Description' in line), None)
        now = datetime.now()
        # 计算 24 小时前的时间
        events = list()
        start_time = now - timedelta(hours=24)
        if header_index is not None:
            lines = lines[header_index+2:]
            i = 0
            while i < len(lines):
                line = lines[i]
                if line and line[0].isdigit():
                    parts = line.split(maxsplit=3)
                    seq = parts[0]
                    date_str = parts[1]
                    time_str = parts[2].split('.')[0]
                    desc = parts[3]
                    i += 1
                    # 处理描述换行
                    while i < len(lines) and not lines[i].startswith('---') and not lines[i][0].isdigit():
                        desc += ' ' + lines[i].strip()
                        i += 1
                    try:
                        event_time = datetime.strptime(f'{date_str} {time_str}', '%Y-%m-%d %H:%M:%S')
                        if event_time >= start_time:
                            events.append((seq, event_time.strftime('%Y-%m-%d %H:%M:%S'), desc))
                    except Exception as e:
                        logger.error("parse time failed, ip:%s cmd: line: %s, %s", cmd.ip, cmd.cmd, line, e)
                else:
                    i += 1
        cmd.analyze = f'{DELIMITER}'.join(' '.join(e) for e in events) if events else CHECK_PASS


class H3CSwitchAnalyzer(object):
    @staticmethod
    def calc_minutes(uptime_str):
        # 正则表达式匹配天数、小时数和分钟数
        if 'week' in uptime_str:
            pattern1 = r'(\d+) weeks?, (\d+) days?, (\d+) hours?, (\d+) minutes?'
        else:
            pattern1 = r'(\d+) days?, (\d+) hours?, (\d+) minutes?'
        match = re.search(pattern1, uptime_str)
        if match:
            weeks = 0
            if len(match.groups()) == 4:
                weeks, days, hours, minutes = map(int, match.groups())
            else:
                days, hours, minutes = map(int, match.groups())
            return weeks * 7 * 24 * 60 + days * 24 * 60 + hours * 60 + minutes
        return 0

    @staticmethod
    def split_line(raw_data: str) -> list:
        if not raw_data:
            return []
        lines = raw_data.split('\n')
        if len(lines) < 3:
            return []
        return lines

    @staticmethod
    def fetch_version(cmd: Cmd):
        """
        获取交换机/路由器/防火墙版本
        :param cmd: display version
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        for line in lines:
            line = line.strip()
            if 'Version' in line:
                if line.startswith('Version'):
                    cmd.analyze = line
                else:
                    cmd.analyze = line.split(',')[-1]
                return

    @staticmethod
    def fetch_m_lag_status(cmd: Cmd):
        """
        设备M-LAG状态检查
        :param cmd: display m-lag system
        :return:
        """
        master = False
        slave = False
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        if len(lines) < 5:
            return
        for line in lines:
            line = line.strip()
            if 'Backup' in line:
                slave = True
            elif 'Master' in line:
                master = True
        if master and slave:
            cmd.analyze = CHECK_PASS
        elif not master:
            cmd.analyze = 'State: Master找到(display dfs-group 1 m-lag)'
        elif not slave:
            cmd.analyze = 'State: Slave找到(display dfs-group 1 m-lag)'
        else:
            cmd.analyze = 'State: Master/State: Slave均未找到(display dfs-group 1 m-lag)'

    @staticmethod
    def fetch_m_lag_port(cmd: Cmd):
        """
        设备端口M-LAG状态检查
        :param cmd: display m-lag summary
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        error_line = ''
        for line in lines:
            line = line.strip()
            if 'Eth-' in line and 'Up' not in line:
                error_line += f'{line}{DELIMITER}'
            elif 'Connection state:' in line and 'succeeded' not in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_firewall_status(cmd: Cmd):
        """
        防火墙双组状态检查
        :param cmd: display hrp state
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        error_line = ''
        for line in lines:
            line = line.strip()
            if 'Connection state:' in line and 'succeeded' not in line:
                error_line = f'{line}{DELIMITER}'
                break
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_stack_status(cmd: Cmd):
        """
        设备堆叠状态检查
        :param cmd: display irf with-peer-device
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        start_index = next((i for i, line in enumerate(lines) if line.startswith('MemberID')), None)
        has_master = False
        has_slave = False
        if start_index is not None:
            lines = lines[start_index + 1:]
            for line in lines:
                if has_slave and has_master:
                    break
                line = line.strip()
                if ' Master ' in line:
                    has_master = True
                elif ' Standby ' in line:
                    has_slave = True
        if any((has_master, has_slave)):
            cmd.analyze = CHECK_PASS if has_master and has_slave else 'ERROR'

    @staticmethod
    def fetch_stack_port_status(cmd: Cmd):
        """
        设备堆叠端口状态检查
        :param cmd: display irf link
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        error_line = ''
        for line in lines:
            line = line.strip()
            if 'DOWN' in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_runtime(cmd: Cmd):
        """
        运行时间
        :param cmd: display version
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        run_time = 0
        for line in lines:
            line = line.strip()
            if 'uptime' in line:
                chips = line.split('uptime is')
                run_time = H3CSwitchAnalyzer.calc_minutes(chips[1])
                break
        prev_result = int(cmd.prevResult) if cmd.prevResult else 0
        cmd.result = str(run_time)
        cmd.analyze = '设备重启' if run_time < prev_result else CHECK_PASS

    @staticmethod
    def fetch_fan(cmd: Cmd):
        """
        风扇检查
        :param cmd: display fan
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        for line in lines:
            line = line.strip()
            if line.startswith('Fan') and 'Normal' not in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_power(cmd: Cmd):
        """
        电源检查
        :param cmd: display power
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        for line in lines:
            line = line.strip()
            if 'Faulty' in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_three_lay_check(cmd: Cmd):
        """
        三层接口状态
        :param cmd: display ip interface brief
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        start_index = next((i for i, line in enumerate(lines) if line.startswith('Interface')), None)
        if start_index is not None:
            port_lines = lines[start_index + 1:]
            for line in port_lines:  # str
                line = line.strip()
                if not line or line.startswith('MGE'):
                    continue
                parts = line.split(maxsplit=4)
                if len(parts) >= 4 and is_ip_address_mask(parts[3]):
                    physical_status = parts[1].strip()
                    protocol_status = parts[2].strip()
                    if not ('up' in physical_status and 'up' in protocol_status):
                        error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or 'OK'

    @staticmethod
    def fetch_port_check(cmd: Cmd):
        """
        端口检查
        :param cmd: display interface brief
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        error_line = ''
        if 'S10506X' in cmd.deviceName or 'S12508G' in cmd.deviceName:
            i = 0
            while i < len(lines):
                start_index = next((i for i, line in enumerate(lines[i:]) if line.startswith('Interface')), None)
                if start_index is not None:
                    i = start_index + 1
                    port_lines = lines[i:]
                    for line in port_lines:
                        chips = line.strip().split(maxsplit=2)
                        i += 1
                        if len(chips) < 3:
                            break
                        elif 'AMD' in chips[1]:
                            continue
                        elif 'DOWN' in chips[1]:
                            error_line += f'{chips[0]}{DELIMITER}'
                i += 1
        elif cmd.cmd == 'display link-aggregation troubleshooting':
            i = 0
            while i < len(lines):
                line = lines[i].strip()
                if '/' in line:
                    i += 1
                    if 'Selected' not in lines[i]:
                        error_line += f'{line}{DELIMITER}'
                i += 1
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_port_utility(cmd: Cmd):
        """
        端口使用率
        :param cmd: display interface brief
        :return:
        """
        if not isinstance(cmd.rawData, dict):
            return
        error_port = ''
        for data in cmd.rawData.values():
            lines = H3CSwitchAnalyzer.split_line(data)
            header_index = next((i for i, line in enumerate(lines) if 'Interface' in line), None)
            if header_index is not None:
                for line in lines[header_index + 1:]:
                    line = line.strip()
                    if not line:
                        break
                    parts = line.split(maxsplit=2)
                    interface = parts[0]
                    uti = float(parts[1])
                    if uti > 50:
                        error_port += f'{interface}使用率高于50{DELIMITER}'
        cmd.analyze = error_port or CHECK_PASS

    @staticmethod
    def fetch_configure(cmd: Cmd):
        """
        配置文件检查
        :param cmd: dir
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        prev_size = cmd.prevResult
        size = ''
        for line in lines:
            line = line.strip()
            if 'startup.cfg' in line:
                try:
                    size = line.split(maxsplit=3)[2]
                except ValueError:
                    logger.error("read config size failed, %s", line)
                break
        cmd.result = size
        cmd.analyze = '文件大小发生变化, 请手动检查' if prev_size and prev_size != size else CHECK_PASS

    @staticmethod
    def fetch_cpu_utility(cmd: Cmd):
        """
        CPU利用率
        :param cmd: display cpu
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        cpu_usage_5min = -1
        for line in lines:
            line = line.strip()
            if not line or 'last 5 minutes' not in line:
                continue
            chips = line.split(maxsplit=1)
            try:
                cpu_usage_5min = int(chips[0].strip('%'))
            except Exception as e:
                logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                raise e
        cmd.analyze = f"CPU使用率过高, 5分钟平均使用率{cpu_usage_5min}%" if cpu_usage_5min > 70 else CHECK_PASS

    @staticmethod
    def fetch_memory_utility(cmd: Cmd):
        """
        内存利用率
        :param cmd: display memory
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        memory_percentage = -1
        for line in lines:
            line = line.strip()
            if not line:
                continue
            try:
                if line.startswith('Mem:'):
                    memory_percentage_str = line.split()[-1].strip().strip('%')
                    memory_percentage = float(memory_percentage_str)
                    break
            except Exception as e:
                logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                raise e
        cmd.analyze = f"内存使用率过高, 平均使用率{memory_percentage}%" if memory_percentage < 30 else CHECK_PASS

    @staticmethod
    def fetch_single_board_status(cmd: Cmd):
        """
        单板运行状态检查
        :param cmd: display device
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_slot = ''
        header_index = next((i for i, line in enumerate(lines) if 'Slot' in line), None)
        if header_index is not None:
            for line in lines[header_index + 1:]:
                chips = line.strip().split(maxsplit=4)
                if len(chips) < 3:
                    continue
                if chips[1] != 'NONE' and chips[2] not in ('Master', 'Standby', 'Normal'):
                    error_slot += f"{chips[1]}{DELIMITER}"
        cmd.analyze = error_slot or CHECK_PASS

    @staticmethod
    def fetch_rx_power(cmd: Cmd):
        """
        收发光功率
        :param cmd: display transceiver diagnosis interface
        :return:
        """
        if not cmd.rawData:
            return
        error_port = set()
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        need_check_ports = cmd.extra_specs.get('ports')
        current_port = None
        i = 0
        while i < len(lines):
            line = lines[i].strip()
            if line.strip().endswith("transceiver diagnostic information:"):
                current_port = line.split(maxsplit=1)[0]
            elif "Current diagnostic parameters" in line:
                i += 2
                try:
                    power = float(lines[i].strip().split()[3])
                except ValueError:
                    continue
                else:
                    if power < -8:
                        error_port .add(current_port)
            elif 'channel' in line:
                i += 1
                flag = False
                while i < len(lines) and lines[i].strip()[0].isdigit():
                    if float(lines[i].strip().split()[2]) < -8:
                        flag = True
                        break
                    i += 1
                if flag:
                    error_port.add(current_port)
                elif current_port in error_port:
                    error_port.remove(current_port)
            i += 1
        if need_check_ports:
            error_port = error_port - need_check_ports
        cmd.analyze = f'{DELIMITER}'.join(error_port) or CHECK_PASS

    @staticmethod
    def fetch_ospf_status(cmd: Cmd):
        """
        路由OSPF协议检查
        :param cmd: display ospf peer
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_interface = ''
        header_index = next((i for i, line in enumerate(lines) if 'Router ID' in line), None)
        if header_index is not None:
            for line in lines[header_index + 1:]:
                line = line.strip()
                if not line:
                    continue
                parts = line.split()
                try:
                    state = parts[4]
                    if 'Full' not in state:
                        error_interface += f'{parts[5]}{DELIMITER}'
                except Exception as e:
                    logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                    raise e
        else:
            error_interface = '/'
        cmd.analyze = error_interface or CHECK_PASS

    @staticmethod
    def fetch_bgp_status(cmd: Cmd):
        """
        路由BGP协议检查
        :param cmd: display bgp peer
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_peer = ''
        header_index = next((i for i, line in enumerate(lines) if line.strip().startswith('Peer ')), None)
        if header_index is not None:
            for line in lines[header_index + 1:]:
                line = line.strip()
                if line:
                    parts = line.split()
                    if len(parts) >= 8:
                        state = parts[7]
                        if state != 'Established':
                            error_peer += f'{parts[0]}{DELIMITER}'
        else:
            error_peer = '/'
        cmd.analyze = error_peer or CHECK_PASS

    @staticmethod
    def fetch_ospf_item_status(cmd: Cmd):
        """
        路由OSPF条目检查
        :param cmd: display ospf routing
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        if cmd.prevResult:
            prev_result = set(json.loads(cmd.prevResult))
        else:
            prev_result = set()
        result = set()
        for line in lines:
            line = line.strip()
            if starts_with_ip(line):
                result.add(line.split(maxsplit=1)[0].strip())
        router = []
        router.extend(prev_result - result)
        router.extend(result - prev_result)
        cmd.result = json.dumps(list(result))
        cmd.analyze = f'{DELIMITER}'.join(router) if router else CHECK_PASS

    @staticmethod
    def fetch_bgp_item_status(cmd: Cmd):
        """
        路由GBP条目检查
        :param cmd: display bgp routing-table ipv4
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return

        header_index = next((i for i, line in enumerate(lines) if 'Network' in line), None)
        if cmd.prevResult:
            prev_result = set(json.loads(cmd.prevResult))
        else:
            prev_result = set()
        result = set()
        pattern = r'(\d{1,3}\.\d{1,3}\.\d{1,3}\.\d{1,3}/\d{1,2})'  # ip address/mask
        if header_index is not None:
            for line in lines[header_index + 1:]:
                if '>' in line:
                    match = re.search(pattern, line)
                    if match:
                        result.add(match.group(1))
        router = []
        router.extend(prev_result - result)
        router.extend(result - prev_result)
        cmd.result = json.dumps(list(result))
        cmd.analyze = f'{DELIMITER}'.join(router) if router else CHECK_PASS

    @staticmethod
    def fetch_ip_table_status(cmd: Cmd):
        """
        路由表条目检查
        :param cmd: display ip routing-table
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        if cmd.prevResult:
            prev_result = set(json.loads(cmd.prevResult))
        else:
            prev_result = set()
        result = set()
        for line in lines:
            line = line.strip()
            if starts_with_ip(line):
                result.add(line.split(maxsplit=1)[0].strip())
        router = []
        router.extend(prev_result - result)
        router.extend(result - prev_result)
        cmd.result = json.dumps(list(result))
        cmd.analyze = f'{DELIMITER}'.join(router) if router else CHECK_PASS

    @staticmethod
    def fetch_m_lag_alarm(cmd: Cmd):
        """
        M_LAG 24小时告警记录
        :param cmd: display m-lag troubleshooting history
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        header_index = next((i for i, line in enumerate(lines) if 'Event Description' in line), None)
        now = datetime.now()
        # 计算 24 小时前的时间
        events = list()
        start_time = now - timedelta(hours=24)
        if header_index is not None:
            lines = lines[header_index + 1:]
            i = 0
            while i < len(lines):
                line = lines[i].strip()
                if line and line[0].isdigit():
                    parts = line.split(maxsplit=2)
                    date_str = parts[0]
                    time_str = parts[1].split('.')[0]
                    desc = parts[2]
                    i += 1
                    # 处理描述换行
                    while i < len(lines) and not lines[i][0].isdigit():
                        desc += ' ' + lines[i].strip()
                        i += 1
                    try:
                        event_time = datetime.strptime(f'{date_str} {time_str}', '%Y-%m-%d %H:%M:%S')
                        if event_time >= start_time:
                            events.append((event_time.strftime('%Y-%m-%d %H:%M:%S'), desc))
                    except Exception as e:
                        logger.error("parse time failed, ip:%s cmd: line: %s, %s", cmd.ip, cmd.cmd, line, e)
                else:
                    i += 1
        cmd.analyze = f'{DELIMITER}'.join(' '.join(e) for e in events) if events else CHECK_PASS

    @staticmethod
    def fetch_stack_alarm(cmd: Cmd):
        """
        堆叠 24小时告警记录
        :param cmd: display stack troubleshooting history
        :return:
        """
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        header_index = next((i for i, line in enumerate(lines) if 'Event Description' in line), None)
        now = datetime.now()
        # 计算 24 小时前的时间
        events = list()
        start_time = now - timedelta(hours=24)
        if header_index is not None:
            lines = lines[header_index + 2:]
            i = 0
            while i < len(lines):
                line = lines[i]
                if line and line[0].isdigit():
                    parts = line.split(maxsplit=3)
                    seq = parts[0]
                    date_str = parts[1]
                    time_str = parts[2].split('.')[0]
                    desc = parts[3]
                    i += 1
                    # 处理描述换行
                    while i < len(lines) and not lines[i].startswith('---') and not lines[i][0].isdigit():
                        desc += ' ' + lines[i].strip()
                        i += 1
                    try:
                        event_time = datetime.strptime(f'{date_str} {time_str}', '%Y-%m-%d %H:%M:%S')
                        if event_time >= start_time:
                            events.append((seq, event_time.strftime('%Y-%m-%d %H:%M:%S'), desc))
                    except Exception as e:
                        logger.error("parse time failed, ip:%s cmd: line: %s, %s", cmd.ip, cmd.cmd, line, e)
                else:
                    i += 1
        cmd.analyze = f'{DELIMITER}'.join(' '.join(e) for e in events) if events else CHECK_PASS

    @staticmethod
    def extract_port(cmd: Cmd) -> set:
        lines = H3CSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return {[]}

        ports = {[]}
        if 'S10506X' in cmd.deviceName or 'S12508G' in cmd.deviceName:
            i = 0
            while i < len(lines):
                start_index = next((i for i, line in enumerate(lines[i:]) if line.startswith('Interface')), None)
                if start_index is not None:
                    i = start_index + 1
                    port_lines = lines[i:]
                    for line in port_lines:
                        chips = line.strip().split(maxsplit=2)
                        i += 1
                        ports.add(chips[0])
                i += 1
        elif cmd.cmd == 'display link-aggregation troubleshooting':
            for line in lines:
                if '/' in line:
                    ports.add(line.strip().split()[0])
        return ports


class SXFSwitchAnalyzer(object):
    @staticmethod
    def split_line(raw_data: str) -> list:
        if not raw_data:
            return []
        lines = raw_data.split('\n')
        if len(lines) < 3:
            return []
        return lines

    @staticmethod
    def fetch_version(cmd: Cmd):
        pass

    @staticmethod
    def fetch_runtime(cmd: Cmd):
        pass

    @staticmethod
    def fetch_cpu_utility(cmd: Cmd):
        """
        CPU利用率
        :param cmd: display cpu
        :return:
        """
        lines = SXFSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        average_cpu_usage = -1
        for line in lines:
            if average_cpu_usage != -1:
                break
            line = line.strip()
            if not line:
                continue
            if 'Average cpu utilization' in line:
                try:
                    average_cpu_usage = int(line.split(':', maxsplit=1)[1].strip().strip('%'))
                except Exception as e:
                    logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                    raise e
        cmd.analyze = f"CPU使用率过高, 平均使用率{average_cpu_usage}%" if average_cpu_usage > 70 else CHECK_PASS

    @staticmethod
    def fetch_memory_utility(cmd: Cmd):
        lines = SXFSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        memory_usage = -1
        for line in lines:
            if memory_usage != -1:
                break
            line = line.strip()
            if not line:
                continue
            if 'Current memory utilization' in line:
                try:
                    memory_usage = int(line.split(':', maxsplit=1)[1].strip().strip('%'))
                except Exception as e:
                    logger.error('fetch %s result on %s, %s, %s', cmd.cmd, cmd.ip, e, cmd.rawData)
                    raise e
        cmd.analyze = f"CPU使用率过高, 平均使用率{memory_usage}%" if memory_usage > 70 else CHECK_PASS

    @staticmethod
    def fetch_fan(cmd: Cmd):
        lines = SXFSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        for line in lines:
            line = line.strip()
            if ('FAN' in line or '_Fan' in line) and 'Normal' not in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_power(cmd: Cmd):
        lines = SXFSwitchAnalyzer.split_line(cmd.rawData)
        if not lines:
            return
        error_line = ''
        for line in lines:
            line = line.strip()
            if 'PSU' in line and 'Normal' not in line:
                error_line += f'{line}{DELIMITER}'
        cmd.analyze = error_line or CHECK_PASS

    @staticmethod
    def fetch_device_alarm(cmd: Cmd):
        pass


def analyze_huawei(cmd: Cmd):
    metric = cmd.metric
    if cmd.rawData is None:
        return
    if metric == "设备版本检查":
        HuaweiSwitchAnalyzer.fetch_version(cmd)
    elif metric == "设备M-LAG状态检查":
        HuaweiSwitchAnalyzer.fetch_m_lag_status(cmd)
    elif metric == '设备端口M-LAG状态检查':
        HuaweiSwitchAnalyzer.fetch_m_lag_port(cmd)
    elif metric == '设备堆叠状态检查':
        HuaweiSwitchAnalyzer.fetch_stack_status(cmd)
    elif metric == '双主状态检查':
        HuaweiSwitchAnalyzer.fetch_firewall_status(cmd)
    elif metric == "运行时间":
        HuaweiSwitchAnalyzer.fetch_runtime(cmd)
    elif metric == "三层接口状态":
        HuaweiSwitchAnalyzer.fetch_three_lay_check(cmd)
    elif metric == "端口检查":
        HuaweiSwitchAnalyzer.fetch_port_check(cmd)
    elif metric == "风扇检查":
        HuaweiSwitchAnalyzer.fetch_fan(cmd)
    elif metric == "电源检查":
        HuaweiSwitchAnalyzer.fetch_power(cmd)
    elif metric == "带宽利用率":
        HuaweiSwitchAnalyzer.fetch_port_utility(cmd)
    elif metric == "收发光功率检查":
        HuaweiSwitchAnalyzer.fetch_rx_power(cmd)
    elif metric == "配置文件检查":
        HuaweiSwitchAnalyzer.fetch_configure(cmd)
    elif metric == "CPU利用率":
        HuaweiSwitchAnalyzer.fetch_cpu_utility(cmd)
    elif metric == "内存利用率":
        HuaweiSwitchAnalyzer.fetch_memory_utility(cmd)
    elif metric == '单板运行状态检查':
        HuaweiSwitchAnalyzer.fetch_single_board_status(cmd)
    elif metric == "路由OSPF协议检查":
        HuaweiSwitchAnalyzer.fetch_ospf_status(cmd)
    elif metric == "路由BGP协议检查":
        HuaweiSwitchAnalyzer.fetch_bgp_status(cmd)
    elif metric == "路由OSPF条目检查":
        HuaweiSwitchAnalyzer.fetch_ospf_item_status(cmd)
    elif metric == "路由GBP条目检查":
        HuaweiSwitchAnalyzer.fetch_bgp_item_status(cmd)
    elif metric == "路由表条目检查":
        HuaweiSwitchAnalyzer.fetch_ip_table_status(cmd)
    elif metric == '设备当前告警记录':
        HuaweiSwitchAnalyzer.fetch_device_alarm(cmd)
    elif metric == "M_LAG 24小时告警记录":  # display stack troubleshooting history
        HuaweiSwitchAnalyzer.fetch_m_lag_alarm(cmd)
    elif metric == "堆叠 24小时告警记录":  # display stack troubleshooting history
        HuaweiSwitchAnalyzer.fetch_stack_alarm(cmd)


def analyze_h3c(cmd: Cmd):
    metric = cmd.metric
    if cmd.rawData is None:
        return
    if metric == "设备版本检查":
        H3CSwitchAnalyzer.fetch_version(cmd)
    elif metric == "设备M-LAG状态检查":
        H3CSwitchAnalyzer.fetch_m_lag_status(cmd)
    elif metric == '设备端口M-LAG状态检查':
        H3CSwitchAnalyzer.fetch_m_lag_port(cmd)
    elif metric == '设备堆叠状态检查':
        H3CSwitchAnalyzer.fetch_stack_status(cmd)
    elif metric == '双主状态检查':
        H3CSwitchAnalyzer.fetch_firewall_status(cmd)
    elif metric == "运行时间":
        H3CSwitchAnalyzer.fetch_runtime(cmd)
    elif metric == "三层接口状态":
        H3CSwitchAnalyzer.fetch_three_lay_check(cmd)
    elif metric == "端口检查":
        H3CSwitchAnalyzer.fetch_port_check(cmd)
    elif metric == "风扇检查":
        H3CSwitchAnalyzer.fetch_fan(cmd)
    elif metric == "电源检查":
        H3CSwitchAnalyzer.fetch_power(cmd)
    elif metric == "带宽利用率":
        H3CSwitchAnalyzer.fetch_port_utility(cmd)
    elif metric == "收发光功率检查":
        H3CSwitchAnalyzer.fetch_rx_power(cmd)
    elif metric == "配置文件检查":
        H3CSwitchAnalyzer.fetch_configure(cmd)
    elif metric == "CPU利用率":
        H3CSwitchAnalyzer.fetch_cpu_utility(cmd)
    elif metric == "内存利用率":
        H3CSwitchAnalyzer.fetch_memory_utility(cmd)
    elif metric == '单板运行状态检查':
        H3CSwitchAnalyzer.fetch_single_board_status(cmd)
    elif metric == "路由OSPF协议检查":
        H3CSwitchAnalyzer.fetch_ospf_status(cmd)
    elif metric == "路由BGP协议检查":
        H3CSwitchAnalyzer.fetch_bgp_status(cmd)
    elif metric == "路由OSPF条目检查":
        H3CSwitchAnalyzer.fetch_ospf_item_status(cmd)
    elif metric == "路由GBP条目检查":
        H3CSwitchAnalyzer.fetch_bgp_item_status(cmd)
    elif metric == "路由表条目检查":
        H3CSwitchAnalyzer.fetch_ip_table_status(cmd)
    elif metric == "M_LAG 24小时告警记录":  # display stack troubleshooting history
        H3CSwitchAnalyzer.fetch_m_lag_alarm(cmd)
    elif metric == "堆叠 24小时告警记录":  # display stack troubleshooting history
        H3CSwitchAnalyzer.fetch_stack_alarm(cmd)


def analyze_sxf(cmd: Cmd):
    metric = cmd.metric
    if cmd.rawData is None:
        return
    if metric == "设备版本检查":
        SXFSwitchAnalyzer.fetch_version(cmd)
    elif metric == "运行时间":
        SXFSwitchAnalyzer.fetch_runtime(cmd)
    elif metric == "风扇检查":
        SXFSwitchAnalyzer.fetch_fan(cmd)
    elif metric == "电源检查":
        SXFSwitchAnalyzer.fetch_power(cmd)
    elif metric == "CPU利用率":
        SXFSwitchAnalyzer.fetch_cpu_utility(cmd)
    elif metric == "内存利用率":
        SXFSwitchAnalyzer.fetch_memory_utility(cmd)


def send_cmd(device: Device):
    """
    给设备发送命令
    :param cmds:
    :param device:
    :param cmd:
    :return:
    """
    global config
    manufacturer_config: dict = config.get(device.manufacturer, None)
    if not manufacturer_config:
        return
    metric: dict = manufacturer_config['METRIC'].copy()
    for key, value in manufacturer_config.items():
        if key in device.name:
            metric.update(value)

    device_type = 'hp_comware' if device.manufacturer == 'h3c' else device.type
    try:
        net_connect = ConnectHandler(device_type=device_type, ip=device.ip, username=device.username,
                                     password=device.password, port=device.port, timeout=300, read_timeout_override=300)
    except Exception as e:
        logger.error("连接%s %s失败, %s", device.manufacturer, device.ip, e)
        return
    else:
        net_connect.send_command('screen-length 0 temporary')

    result_dict = {}

    for metric_id, cmd in metric.items():
        if not cmd:
            continue
        ret: Cmd = Cmd()
        ret.manufacturer = device.manufacturer
        ret.deviceName = device.name
        ret.ip = device.ip
        ret.metric = metric_id
        ret.cmd = cmd
        result_dict[metric_id] = ret
        if isinstance(ret.cmd, str):
            try:
                logger.info('execute %s on %s %s %s', ret.cmd, ret.manufacturer, ret.deviceName, ret.ip)
                output = net_connect.send_command(ret.cmd)
                if 'huawei' == ret.manufacturer and any(e in output for e in HUAWEI_ERROR_MSG):
                    continue
                elif 'h3c' == ret.manufacturer and any(e in output for e in H3C_ERROR_MSG):
                    continue
                ret.rawData = output
            except Exception as e:
                logger.error('Send %s to %s %s %s failed, %s', ret.cmd, ret.manufacturer, ret.deviceName, ret.ip, e)
        elif isinstance(ret.cmd, list):
            output = {}
            for c in ret.cmd:
                try:
                    logger.info('execute %s on %s %s %s', c, ret.manufacturer, ret.deviceName, ret.ip)
                    ret_str = net_connect.send_command(c)
                    if 'huawei' == ret.manufacturer and ('Unrecognized command' in output or 'not enabled' in output or "Error:" in output):
                        continue
                    elif 'h3c' == ret.manufacturer and ('not configured' in output or 'Unrecognized command' in output):
                        continue
                    output[c] = ret_str
                except Exception as e:
                    logger.error('Send %s to %s %s %s failed, %s', c, ret.manufacturer, ret.deviceName, ret.ip, e)
            ret.rawData = output
        time.sleep(1)

    rx_power = result_dict['收发光功率检查']
    if device.manufacturer == 'huawei':
        HuaweiSwitchAnalyzer.extract_port(result_dict['端口检查'])
    else:
        H3CSwitchAnalyzer.extract_port(result_dict['端口检查'])
    rx_power.extra_specs['ports'] = result_dict['端口检查'].result
    return list(result_dict.values())


def read_device_info(file_name):
    info_sheet = None
    try:
        wb = openpyxl.load_workbook(file_name)
        info_sheet = wb['信息表']
    except FileNotFoundError:
        logger.error(f"错误：未找到文件 {file_name}")
    except KeyError:
        logger.error(f"错误：未找到工作表 信息表/命令集")
    except Exception as e:
        logger.error(f"发生未知错误：{e}")

    # 读取设备信息
    dev_list = list()
    for row in range(2, info_sheet.max_row + 1):
        device = Device()
        device.name = info_sheet['a' + str(row)].value.strip()
        device.manufacturer = info_sheet['b' + str(row)].value.strip()
        device.type = device.manufacturer.strip()
        device.ip = info_sheet['c' + str(row)].value.strip()
        device.username = info_sheet['d' + str(row)].value.strip()
        device.password = info_sheet['e' + str(row)].value.strip()
        dev_list.append(device)
    # dev_list = [e for e in dev_list if e.ip == '38.6.255.1']
    return dev_list


def dump_analyze(file_path: str, cmd_executed_list: list[Cmd], dev_list: list[Device]):
    metrics = list(set(e.metric for e in cmd_executed_list))
    metrics.sort()
    headers = ["厂商", "设备名称", "设备地址", '设备版本检查', '运行时间', '设备M-LAG状态检查', '设备端口M-LAG状态检查', '设备堆叠状态检查', '双主状态检查', '风扇检查', '电源检查',
               '三层接口状态', '端口检查', '带宽利用率', '配置文件检查', 'CPU利用率', '内存利用率', '单板运行状态检查', '收发光功率检查', '路由OSPF协议检查',
               '路由BGP协议检查', '路由OSPF条目检查', '路由GBP条目检查', '路由表条目检查', '设备当前告警记录',
               'M_LAG 24小时告警记录', '堆叠 24小时告警记录', '设备堆叠端口状态检查']

    cmd_executed_dict = defaultdict(list)
    for cmd_ in cmd_executed_list:
        cmd_executed_dict[cmd_.ip].append(cmd_)

    with open(file_path, 'w+', encoding='utf-8-sig', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=headers, restval='/')
        writer.writeheader()
        for dev in dev_list:
            ip_cmds = cmd_executed_dict[dev.ip]
            if not ip_cmds:
                continue
            if dev.manufacturer == 'huawei':
                analyze_cmd = analyze_huawei
            elif dev.manufacturer == 'h3c':
                analyze_cmd = analyze_h3c
            else:
                analyze_cmd = analyze_sxf
            for ip_cmd in ip_cmds:
                analyze_cmd(ip_cmd)
            data = {e.metric: e.result for e in ip_cmds}
            data['厂商'] = dev.manufacturer
            data['设备名称'] = dev.name
            data['设备地址'] = dev.ip


def dump_detail(file_path: str, cmd_executed_list: list[Cmd], dev_list: list[Device]):
    metrics = list(set(e.metric for e in cmd_executed_list))
    metrics.sort()
    headers = ["厂商", "设备名称", "设备地址", '设备版本检查', '运行时间', '设备M-LAG状态检查', '设备堆叠状态检查', '双主状态检查', '风扇检查', '电源检查',
               '单板运行状态检查', 'CPU利用率', '内存利用率', '带宽利用率', '收发光功率检查', '路由OSPF协议检查',
               '路由BGP协议检查', '路由表条目检查', '设备当前告警记录']

    cmd_executed_dict = defaultdict(list)
    for cmd_ in cmd_executed_list:
        cmd_executed_dict[cmd_.ip].append(cmd_)

    with open(file_path, 'w+', encoding='utf-8-sig', newline='') as f:
        writer = csv.DictWriter(f, fieldnames=headers, restval='/')
        writer.writeheader()
        for dev in dev_list:
            ip_cmds = cmd_executed_dict[dev.ip]
            if not ip_cmds:
                continue
            data = {e.metric: e.result for e in ip_cmds}
            data['厂商'] = dev.manufacturer
            data['设备名称'] = dev.name
            data['设备地址'] = dev.ip
            writer.writerow(data)


def run():
    logger.info("------------脚本启动执行----------")
    executor = ThreadPoolExecutor(max_workers=10)
    today = date.today()

    date_time = today.strftime("%Y%m")  # 获取本地时间,打印效果：202208

    output_path = os.path.join('output', date_time)  # 文件路径，在此目录下存放不同时间段生成的备份目录
    if not os.path.exists(output_path):  # 判断文件夹是否存在，如有同名文件夹则删除
        os.makedirs(output_path)

    conf_file = '网络设备.xlsx'
    dev_list = read_device_info(conf_file)

    futures = []
    for device in dev_list:
        future = executor.submit(send_cmd, device)
        futures.append(future)

    cmd_executed_list = list()
    for future in concurrent.futures.as_completed(futures):
        result = future.result()
        if result:
            cmd_executed_list.extend(result)

    interval = 1
    try:
        # 按主键降序排序，取第一条记录
        last_record: Network = Network.select().order_by(Network.id.desc()).get()
    except DoesNotExist:
        logger.warning("未找到上一次执行记录")
    else:
        interval = last_record.interval

    last_records = Network.select().where(Network.interval == interval)
    last_cmd_dict = {f'{e.ip}{e.manufacturer}{e.metric}': e for e in last_records}

    cmd_executed_list = [e for e in cmd_executed_list if e.rawData]
    for cmd in cmd_executed_list:
        key = f'{cmd.ip}{cmd.manufacturer}{cmd.metric}'
        if key in last_cmd_dict:
            cmd.prevResult = last_cmd_dict[key].result

    file_suffix = f'{datetime.now().strftime("%Y%m%d-%H%M")}.csv'
    try:
        dump_analyze(os.path.join(output_path, f'设备检查结果_{file_suffix}'), cmd_executed_list, dev_list)
        insert_query = Network.insert_many([e.to_db(interval+1) for e in cmd_executed_list if e.result])
        insert_query.execute()
    finally:
        db.close()

    dump_detail(os.path.join(output_path, f'设备检查详情_{file_suffix}'), cmd_executed_list, dev_list)
    logger.info("---------------脚本执行完成------------")


if __name__ == '__main__':
    run()
